圖:“Rust 的吉祥物 Ferris the Crab 把紙箱整理放進去大木箱裡”,gemini-2.5-flash-preview,2025年09月17日。
都說了 Rust 重編譯,cargo
專案提供了強大穩定的骨架,本篇會實作我是如何將小元件的實作,成長到模組化,希望帶出其結構邊界,讓開發可以在可控的邊界內成長。
另外,值得在這系列文章的開端提一提的就是在現在 AI LLM coding 的時代下,我個人有個小小的技法,就是從抽象海洋中往下逐一實例化。AI 有多強無所謂,開發者還是人,人還是人,之所以讓開發者沈浸其中的開發工作,不就是將腦海中很跳躍的創意想法,一步一腳印把想法給實作建構起來嗎?那現在還是所謂 “Agentic AI” 的時代(?,能夠幫你寫程式碼,還能夠幫你下指令執行,錯誤會自己看自己修正,往復循環直到成功了才提醒你過來看。身為 Rust 開發者,最最最重要的是它幫你擋下編譯器的炮口了,終於...可以擺脫被 Rust 編譯器支配的恐懼,愛死你了~
我會儘量讓我的系列文章從抽象海洋描述,再到示意圖與結構,最後才是實例化的程式碼範例。當工程師遇到文件,心裡慌得一批,我需要模板...沒事!自己先建構這個樣子試試!
等等要實作的專案結構先破題:
cargo_tutorial/
├── Cargo.toml # 專案配置
├── src/
│ ├── lib.rs # 程式庫根檔案
│ ├── main.rs # 應用程式入口(執行檔入口)
│ ├── csv_converter/ # 模組 module (mod)
│ │ ├── mod.rs # 模組介面
│ │ └── converter.rs # 邏輯實作
│ └── test_data_generator.rs # 獨立邏輯實作,亦可以視為模組
└── README.md # 記得!寫文件!XD
上圖解釋:
想像你的整個專案(cargo new cargo_tutorial
)就是一個大木箱 crate
,這是 Rust 編譯的最小單位,lib.rs
就是貼在木箱的便利貼,告訴你箱子裡面有什麼模組可以拿來用,main.rs
告訴你編譯後我就是執行檔入口,沒有 main.rs
就是函式庫的專案,在 Rust 中的各種依賴就是如此。
大箱子內的小紙箱就是模組 module mod
關鍵字,一個檔案、一個資料夾都可以成為一個模組,模組內也可以有子模組,以此類推。
cargo_tutorial/
├── Cargo.toml # 專案配置
├── src/
│ ├── lib.rs # 程式庫根檔案
│ ├── main.rs # 應用程式入口(執行檔入口)
│ ├── csv_converter/ # 模組 module (mod)
│ │ ├── mod.rs # 模組介面
│ │ └── converter.rs # 邏輯實作
│ └── test_data_generator.rs # 獨立邏輯實作,亦可以視為模組
└── README.md # 記得!寫文件!XD
流程開始
↓
main() 應用程式入口:軟體啟動!
↓
呼叫 create_sample_csv_file("demo.csv", 50)
├─► 建立 CSV Writer
├─► 寫入標題列 ["id", "name", "age", "city", "value", "active"]
├─► 產生 50 筆測試資料迴圈
│ ├─► 生成使用者名稱、年齡、城市等欄位
│ └─► 寫入每一行資料
└─► 關閉檔案並返回成功
↓
呼叫 CsvConverter::convert_csv_to_json_file("demo.csv", "demo.json")
├─► 開啟 demo.csv 檔案
├─► 建立 CSV 讀取器
├─► 讀取標題列
├─► 處理每一行資料迴圈
│ ├─► 將每一欄位轉換為適當的 JSON 型別
│ │ ├─► 整數 → Number
│ │ ├─► 浮點數 → Number
│ │ ├─► "true"/"false" → Boolean
│ │ └─► 其他 → String
│ └─► 建立鍵值對 HashMap
├─► 將所有記錄序列化為 JSON 字串
├─► 寫入 demo.json 檔案
└─► 返回成功
↓
流程結束,查看第一個函數所生成的 CSV 檔案是否有順利被第二個函數轉換成 JSON 檔案;demo.csv 與 demo.json
# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.3"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
// src/main.rs
// 模組引入
mod csv_converter;
mod test_data_generator;
// 模組使用
use csv_converter::CsvConverter;
use test_data_generator::create_sample_csv_file;
fn main() -> std::io::Result<()> {
// 生成測試用 CSV 檔案,讓接下來的函數有 CSV 資料檔案使用
create_sample_csv_file("demo.csv", 50)?;
// 將 CSV 轉換為 JSON 格式,並儲存為 demo.json 檔案
CsvConverter::convert_csv_to_json_file("demo.csv", "demo.json")?;
Ok(())
}
// src/test_data_generator.rs
use std::fs::File;
use csv::Writer;
pub fn create_sample_csv_file(filename: &str, record_count: usize) -> std::io::Result<()> {
let file = File::create(filename)?;
let mut writer = Writer::from_writer(file);
// 寫入 CSV 標題列
writer.write_record(&["id", "name", "age", "city", "value", "active"])?;
let cities = ["New York", "London", "Tokyo", "Paris"];
// 產生測試記錄(為簡單起見使用確定性生成)
for i in 1..=record_count {
let name = format!("User{}", i);
let age = 20 + (i % 50);
let city = cities[i % cities.len()];
let value = 100.0 + (i as f64 * 10.0);
let active = (i % 3) != 0;
writer.write_record(&[
&i.to_string(),
&name,
&age.to_string(),
city,
&format!("{:.2}", value),
&active.to_string()
])?;
}
writer.flush()?;
Ok(())
}
// src/csv_converter/converter.rs
use csv::Reader;
use std::fs::File;
use std::collections::HashMap;
// 宣告公開結構體,讓外部的 crate 或其他模組可以存取 CsvConverter 功能
pub struct CsvConverter;
// 實作 CsvConverter 結構體的方法
impl CsvConverter {
/// 將 CSV 檔案轉換並儲存為 JSON 檔案
pub fn convert_csv_to_json_file(csv_path: &str, json_path: &str) -> std::io::Result<()> {
// 開啟 CSV 檔案
let file = File::open(csv_path)?;
let mut reader = Reader::from_reader(file);
// 讀取標題行
let headers: Vec<String> = reader.headers()?
.iter()
.map(|h| h.to_string())
.collect();
let mut records = Vec::new();
// 處理每一行資料
for result in reader.records() {
let record = result?;
let mut row_map = HashMap::new();
// 將每一行轉換為鍵值對
for (i, field) in record.iter().enumerate() {
if i < headers.len() {
let header = &headers[i];
let value = if let Ok(num) = field.parse::<i64>() {
serde_json::Value::Number(num.into())
} else if let Ok(num) = field.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(num) {
serde_json::Value::Number(n)
} else {
serde_json::Value::String(field.to_string())
}
} else if field.to_lowercase() == "true" {
serde_json::Value::Bool(true)
} else if field.to_lowercase() == "false" {
serde_json::Value::Bool(false)
} else {
serde_json::Value::String(field.to_string())
};
row_map.insert(header.clone(), value);
}
}
records.push(row_map);
}
// 將記錄序列化為 JSON
let json_string = serde_json::to_string_pretty(&records)?;
// 輸出 JSON 檔案
std::fs::write(json_path, &json_string)?;
Ok(())
}
}
// src/csv_converter/mod.rs
// 宣告子模組,告訴編譯器在同一個目錄中尋找 converter.rs 檔案
mod converter;
// 重新匯出所有公開項目,讓外部可以使用這個模組的公開介面
pub use converter::*;
// src/lib.rs
// 宣告公開模組,讓外部 crate 可以存取 csv_converter 功能
pub mod csv_converter;
// 重新匯出核心功能,讓使用者更容易使用
pub use csv_converter::CsvConverter;
lib.rs
與 mod.rs
撰寫規則語法上這兩者幾乎是一樣的,但各自的角色和作用域是不一樣的,一個大木箱 crate
或嚴謹一點稱之為 crate root
中只能有一個 lib.rs
,很好理解,當然只能有一份能夠宣告我有什麼可以拿來用。mod.rs
是在這個大木箱內可能有很多個小紙箱模組 mod
或 module root
的空間宣告你可以從我這個小紙箱拿什麼東西出來用。
簡單來說,你開發的專案 crate
要給別人用的時候,lib.rs
對外,mod.rs
對內,編譯到你的 crate
的時候,編譯器會先找到 lib.rs
,根據上面寫的進行編譯。
// 找到 module_name.rs 或 module_name/mod.rs
pub mod module_name;
// 找到 internal.rs 或 internal/mod.rs,但不對外公開使用,作用域是在整個 crate 內部,因為現在只有 src/ 所呈現的一個 crate,在非 workspace 情況下呈現不出私有的感覺
mod internal;
// 重新匯出特定型別,結構體實作:pub struct SomeType
pub use module_name::SomeType;
// 重新匯出特定函數,功能實作:pub fn some_function(){}
pub use module_name::some_function;
// 重新匯出所有公開項目 (萬用字元)
pub use module_name::*;